﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;
using System.Net.Cache;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace ForumReader
{
    public partial class MainForm : Form
    {
        private ForumClient forum;

        public MainForm()
        {
            InitializeComponent();
        }

#if false
        private void mnuEditSync_Click(object sender, EventArgs e)
        {
            // Sync
            // Get the board to sync
            string boardName = "PPPerson";

            BoardReader boardReader = new BoardReader(); // SjtuBoardReader();
            boardReader.BoardName = boardName;
            // boardReader.ReadDirectory();
            PostEntry[] posts = boardReader.ParseDirectoryPageFromFile("tt1.txt");

            // Add this to the list
            lvPosts.Items.Clear();
            for (int i = 0; i < posts.Length; i++)
            {
                ListViewItem item = new ListViewItem();
                item.SubItems.Add(posts[i].Date);
                item.SubItems.Add(posts[i].Author);
                item.SubItems.Add(posts[i].Title);
                lvPosts.Items.Add(item);
            }
        }
#endif

        private void btnTest_Click(object sender, EventArgs e)
        {
            //LoadBoardsAsync();
        }

        // We use an async pattern to retrieve data from the web. At any time,
        // there may be the following background tasks running:
        // 1 - retrieving board list
        // 2 - retrieving thread list in a given board
        // 3 - retrieving posts in a given thread
        // 4 - downloading attachments in a given post
        //
        // When we start a new task of one type, we want to stop the currently
        // running task of that type in order to reduce network traffic and
        // avoid retrieving many items that the user won't actually look at.
        // To achieve this, we create a cancellation token source for the
        // currently running task of each type, and keep a handle to it. When
        // we start a new task of a given type, we signal the existing 
        // cancellation token to cancel the existing task.
        //
        // To ensure that the above pattern work properly, the following rules
        // must be followed:
        // 1 - Any code that modifies the cancellation token sources must be 
        //     executed from the UI thread.
        // 2 - Any async code that modifies the UI must check the cancellation
        //     token each time a continuation block is entered (i.e. after
        //     each await statement).
        // 3 - OperationCancelledException must be handled to prevent the UI
        //     from aborting.

        private CancellationTokenSource ctsLoadBoards;
        private CancellationTokenSource ctsLoadThreads;
        private CancellationTokenSource ctsLoadPosts;

        private static void CancelTask(ref CancellationTokenSource cts)
        {
            if (cts != null)
            {
                cts.Cancel();
                cts = null;
            }
        }

        private static CancellationToken RestartTask(ref CancellationTokenSource cts)
        {
            if (cts != null)
                cts.Cancel();
            cts = new CancellationTokenSource();
            return cts.Token;
        }

        private async void LoadBoardsAsync()
        {
            CancellationToken ct = RestartTask(ref ctsLoadBoards);

            forum = new IchigoForumClient();

            // Reset the tree view.
            tvBoards.Nodes.Clear();
            TreeNode root = tvBoards.Nodes.Add(forum.ForumTitle);

            // Retrieve boards asynchronously.
            IEnumerator<ForumBoard[]> boardsIterator = await Task.Run(
                () => forum.GetBoards(ct).GetEnumerator(), ct);
            if (ct.IsCancellationRequested)
                return;
#if DEBUG
            System.Diagnostics.Debug.WriteLine("Enumerator ready");
#endif

            // Iterate through the boards asynchronously.
            while (await Task.Run(() => boardsIterator.MoveNext(), ct))
            {
                if (ct.IsCancellationRequested)
                    return;
                foreach (ForumBoard board in boardsIterator.Current)
                {
                    TreeNode node = new TreeNode();
                    node.Text = board.Title + " (" + board.ThreadCount + ")";
                    node.Tag = board;
                    root.Nodes.Add(node);
                }
                root.Expand();
            }
            if (ct.IsCancellationRequested)
                return;
        }

        private async void LoadThreadsAsync(string boardId)
        {
            CancellationToken ct = RestartTask(ref ctsLoadThreads);

            lvThreads.Items.Clear();
            lvThreads.Enabled = false;
            IEnumerator<ForumThread[]> threadsIterator = await Task.Run(
                () => forum.GetThreads(boardId, ct).GetEnumerator(), ct);
            if (ct.IsCancellationRequested)
                return;

            while (await Task.Run(() => threadsIterator.MoveNext(), ct))
            {
                if (ct.IsCancellationRequested)
                    return;
                foreach (ForumThread thread in threadsIterator.Current)
                {
                    ListViewItem item = new ListViewItem();
                    item.SubItems.Add(thread.Date.ToString());
                    item.SubItems.Add(thread.Title);
                    item.Tag = thread;
                    lvThreads.Items.Add(item);
                }
            }
            if (ct.IsCancellationRequested)
                return;
            lvThreads.Enabled = true;
        }

        private ForumAttachment[] currentAttachments;

        private async void LoadPostsAsync(string threadId)
        {
            CancellationToken ct = RestartTask(ref ctsLoadPosts);

            // Clear the UI to indicate reloading.
            lvImages.Enabled = false;
            lvImages.VirtualListSize = 0;

            // Retrieve posts.
            IEnumerator<ForumPost[]> postsIterator = await Task.Run(
                () => forum.GetPosts(threadId, ct).GetEnumerator(), ct);
            if (ct.IsCancellationRequested)
            {
#if DEBUG
                System.Diagnostics.Debug.WriteLine("LoadPostsAsync(" +
                    threadId + "): cancelled.");
#endif
                return;
            }

#if false
            // Let's just create 15 dummy items.
            for (int j = 0; j < 15; j++)
            {
                ListViewItem item = new ListViewItem();
                item.Text = j.ToString();
                item.Tag = new ForumAttachment();
                lvImages.Items.Add(item);
            }
            System.Diagnostics.Debug.WriteLine("LoadPostsAsync" +
                "(" + threadId + "): loaded 15 dummy items.");
            lvImages.Visible = true;
            return;
#endif

            // Collect the attachments of all the posts. Since this code runs
            // in the UI thread, there is no need to check cancellation here.
            List<ForumAttachment> attachments = new List<ForumAttachment>();
            while (await Task.Run(() => postsIterator.MoveNext(), ct))
            {
                if (ct.IsCancellationRequested)
                    return;
                foreach (ForumPost post in postsIterator.Current)
                {
                    attachments.AddRange(post.Attachments.Where(
                        (ForumAttachment a) => (a.Type == ForumAttachmentType.Image)));
                }
            }
            if (ct.IsCancellationRequested)
                return;

            // Update UI.
            currentAttachments = attachments.ToArray();
            lvImages.VirtualListSize = currentAttachments.Length;
            //lvImages.Visible = true;
            lvImages.Enabled = true;
            lvImages.Invalidate();
            
            if (currentAttachments.Length > 0)
                lvImages.SelectedIndices.Add(0);

#if DEBUG
            System.Diagnostics.Debug.WriteLine("LoadPostsAsync" +
                "(" + threadId + "): loaded " + attachments.Count + " images");
#endif
            //return;

#if false
            // Start a background thread to download the attachments.
            // TODO: we need to Dispose() the images sometime.
            Task.Factory.StartNew(() =>
            {
                WebClient client = new WebClient();
                client.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable);

                foreach (ForumAttachment a in attachments)
                {
                    if (ct.IsCancellationRequested)
                    {
#if DEBUG
                        System.Diagnostics.Debug.WriteLine("LoadPostsAsync(" +
                            threadId + "): download cancelled.");
#endif
                        return;
                    }

                    // TODO: pass cancellation token and check for exception.
                    byte[] data = client.DownloadData(a.Source);
                    a.Size = data.Length;
                    using (Stream stream = new MemoryStream(data))
                    {
                        a.Object = Image.FromStream(stream);
                        // catch (ArgumentException)
                    }
                    this.BeginInvoke(() => { item.Text = "OK"; });
                }
            });
#endif

            // Download the attachments one at a time in a separate thread.
            // TODO: we need to Dispose() the images sometime.
            WebClient client = new WebClient();
            client.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable);
            int i = 0;
            foreach (ForumAttachment a in attachments)
            {
                // TODO: pass cancellation token and check for exception.
                await Task.Run(() =>
                {
                    byte[] data = client.DownloadData(a.Source);
                    a.Size = data.Length;
                    using (Stream stream = new MemoryStream(data))
                    {
                        a.Object = Image.FromStream(stream);
                        // catch (ArgumentException)
                    }
                });
                if (ct.IsCancellationRequested)
                {
#if DEBUG
                    System.Diagnostics.Debug.WriteLine("LoadPostsAsync(" +
                        threadId + "): download cancelled.");
#endif
                    return;
                }

                if (i < lvImages.VirtualListSize)
                    lvImages.RedrawItems(i, i, true);
                // lvImages.Items[i].Text = "OK";
                i++;
            }
            txtStatus.Text = attachments.Count + " attachments downloaded.";
        }

        private ForumAttachment GetActiveAttachment()
        {
            if (lvImages.SelectedIndices.Count == 0)
                return null;
            int i = lvImages.SelectedIndices[0];
            if (i >= currentAttachments.Length)
                return null;
            return currentAttachments[i];
        }

        private void lvImages_SelectedIndexChanged(object sender, EventArgs e)
        {
            ForumAttachment a = GetActiveAttachment();
            if (a == null || a.Object == null)
            {
                txtStatus.Text = "";
                return;
            }
            Image img = a.Object as Image;
            txtImageDimension.Text = img.Width + " x " + img.Height;
            txtImageSize.Text = (a.Size / 1024) + " KB";
            txtImageSource.Text = a.Source;
#if true
            pictureBox1.Invalidate();
#else
            if (pictureBox1.Image != null)
                pictureBox1.Image.Dispose();
            pictureBox1.Image = img;
#endif
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            tvBoards.SetWindowTheme("explorer");
            lvThreads.SetWindowTheme("explorer");
            lvImages.SetWindowTheme("explorer");
            lvThreads.HideFocus();
            LoadBoardsAsync();
        }

        private void lvThreads_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (btnTest.Checked)
                return;
            if (lvThreads.SelectedItems.Count == 0)
                return;
            ForumThread thread = lvThreads.SelectedItems[0].Tag as ForumThread;
            LoadPostsAsync(thread.Id);
        }

        private void lvThreads_MouseDoubleClick(object sender, MouseEventArgs e)
        {
#if false
            if (lvThreads.SelectedItems.Count == 0)
                return;
            ForumThread thread = lvThreads.SelectedItems[0].Tag as ForumThread;
            LoadPostsAsync(thread.Id);
#endif
        }

        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            if (pictureBox1.Image != null)
                return;

            ForumAttachment a = GetActiveAttachment();
            if (a == null)
                return;

            Image img = a.Object as Image;
            if (img == null)
                return;

            // Center the image in the picture box.
            Rectangle rect = new Rectangle(0, 0, img.Width, img.Height);
            Size box = pictureBox1.ClientSize;
            if (rect.Width > box.Width)
            {
                rect.Height = rect.Height * box.Width / rect.Width;
                rect.Width = box.Width;
            }
            if (rect.Height > box.Height)
            {
                rect.Width = rect.Width * box.Height / rect.Height;
                rect.Height = box.Height;
            }
            rect.X = e.ClipRectangle.X + (box.Width - rect.Width) / 2;
            rect.Y = e.ClipRectangle.Y + (box.Height - rect.Height) / 2;

            e.Graphics.DrawImage(img, rect);
        }

        private void tvBoards_AfterSelect(object sender, TreeViewEventArgs e)
        {

        }

        private void tvBoards_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            TreeNode node = tvBoards.SelectedNode;
            if (node == null)
                return;

            if (node.Tag is ForumBoard)
            {
                LoadThreadsAsync((node.Tag as ForumBoard).Id);
            }
        }

        private void mnuThreadOpenInBrowser_Click(object sender, EventArgs e)
        {
            if (lvThreads.SelectedItems.Count == 0)
                return;
            ForumThread thread = lvThreads.SelectedItems[0].Tag as ForumThread;

            // See http://stackoverflow.com/questions/5501349/open-website-in-the-users-default-browser-without-letting-them-launch-anything
            // to prevent remote attack.
            System.Diagnostics.Process.Start(thread.Link);
        }

        private void mnuThreadCopyTitle_Click(object sender, EventArgs e)
        {
            if (lvThreads.SelectedItems.Count == 0)
                return;
            ForumThread thread = lvThreads.SelectedItems[0].Tag as ForumThread;
            Clipboard.SetText(thread.Title);
        }

        private void mnuThreadCopyUrl_Click(object sender, EventArgs e)
        {
            if (lvThreads.SelectedItems.Count == 0)
                return;
            ForumThread thread = lvThreads.SelectedItems[0].Tag as ForumThread;
            Clipboard.SetText(thread.Link);
        }

        private void lvImages_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
        {
            if (currentAttachments == null)
                return;
            if (e.ItemIndex >= currentAttachments.Length)
                return;

            ForumAttachment a = currentAttachments[e.ItemIndex];
            ListViewItem item = new ListViewItem();
            item.Text = (a.Object == null) ? a.Source : "OK";
            e.Item = item;
        }
    }
}
